// src/node.js
const DHT = require('bittorrent-dht');
const dgram = require('dgram');
const crypto = require('crypto');
const sodium = require('sodium-native');
const EventEmitter = require('events');
const fs = require('fs').promises;
const StunClient = require('../lib/stunClient');
const ConnectionManager = require('../lib/connectionManager');

/**
 * A P2P Node that uses a DHT for discovery and establishes encrypted direct connections.
 * Operates on a single UDP port for both DHT and application traffic.
 */
class P2PNode extends EventEmitter {
  /**
   * Creates a new P2PNode instance.
   * @param {Object} options - Configuration options.
   * @param {number} options.port - The UDP port to listen on.
   * @param {Array} options.bootstrapNodes - List of DHT bootstrap nodes.
   * @param {string|null} [options.nodeId=null] - Optional fixed node ID.
   * @param {boolean} [options.enableEncryption=true] - Enable end-to-end encryption.
   * @param {string} [options.identityFile=null] - Path to file for persistent identity.
   * @param {Array} [options.stunServers=[]] - List of STUN servers.
   * @param {string} [options.signalingServerUrl='http://localhost:3000'] - URL of signaling server.
   */
  constructor(options = {}) {
    super();
    this.port = options.port || 6881;
    this.bootstrapNodes = options.bootstrapNodes || [];
    this.enableEncryption = options.enableEncryption !== false; // Default true
    this.identityFile = options.identityFile || null;
    this.stunServers = options.stunServers || [];
    this.signalingServerUrl = options.signalingServerUrl || 'http://localhost:3000';
    
    // Generate or load Node ID
    this.nodeId = options.nodeId ? Buffer.from(options.nodeId, 'hex') : null;
    if (!this.nodeId) {
      this.nodeId = crypto.randomBytes(20); // Standard DHT node ID length
    }

    this.dht = null;
    this.udpSocket = null;
    this.peers = new Map(); // Map of connected peers
    this.pendingConnections = new Map(); // Map of ongoing connection attempts
    this.isRunning = false;
    this.publicAddress = null;

    // Encryption keys (simplified for example)
    this.keyPair = null;
    
    // Connection manager for handling P2P connections
    this.connectionManager = new ConnectionManager(this);
    
    // Signaling server connection
    this.socket = null;
  }

  /**
   * Starts the P2P node: initializes DHT, binds UDP socket, loads identity.
   */
  async start() {
    if (this.isRunning) {
      throw new Error('Node is already running');
    }

    await this._loadIdentity();
    this.keyPair = sodium.crypto_kx_keypair(); // Generate keypair for encryption

    // 1. Create and bind the single UDP socket
    this.udpSocket = dgram.createSocket('udp4');
    this.udpSocket.on('message', this._handleUdpMessage.bind(this));
    this.udpSocket.on('error', (err) => {
      console.error('UDP Socket Error:', err);
      this.emit('error', err);
    });

    await new Promise((resolve, reject) => {
      this.udpSocket.bind(this.port, (err) => {
        if (err) reject(err);
        else resolve();
      });
    });

    console.log(`UDP socket bound to port ${this.port}`);

    // 2. Initialize the DHT instance using the same socket
    this.dht = new DHT({
      nodeId: this.nodeId,
      socket: this.udpSocket, // Crucial: Reuse the same socket
      bootstrap: this.bootstrapNodes
    });

    this.dht.on('ready', () => {
      console.log('DHT is ready');
      this.emit('dhtReady');
    });

    this.dht.on('peer', this._onDhtPeer.bind(this));
    this.dht.on('error', (err) => {
      console.error('DHT Error:', err);
      this.emit('error', err);
    });

    // Start the DHT
    this.dht.listen();

    // Get our public address for NAT traversal
    await this._getPublicAddress();
    
    this.isRunning = true;
    console.log(`P2P Node started with ID: ${this.nodeId.toString('hex')}`);
    
    // Connect to the signaling server for connection establishment
    await this._connectToSignalingServer();
  }

  /**
   * Stops the P2P node: closes DHT, UDP socket, cleans up.
   */
  async stop() {
    if (!this.isRunning) {
      return;
    }

    await this._saveIdentity();

    if (this.dht) {
      this.dht.destroy();
    }
    if (this.udpSocket) {
      this.udpSocket.close();
    }
    
    // Disconnect from signaling server
    if (this.socket) {
      this.socket.disconnect();
    }

    this.isRunning = false;
    console.log('P2P Node stopped');
  }

  /**
   * Announces this node's presence for a specific topic in the DHT.
   * @param {string|Buffer} topic - The topic to announce under.
   */
  announce(topic) {
    if (!this.dht || !this.isRunning) {
      throw new Error('Node must be started before announcing');
    }
    const topicBuffer = Buffer.isBuffer(topic) ? topic : Buffer.from(topic);
    this.dht.announce(topicBuffer, this.port, (err) => {
      if (err) {
        console.error(`Failed to announce for topic ${topicBuffer.toString('hex')}:`, err);
      } else {
        console.log(`Announced for topic ${topicBuffer.toString('hex')}`);
      }
    });
  }

  /**
   * Looks up peers for a specific topic in the DHT.
   * @param {string|Buffer} topic - The topic to lookup.
   * @param {Function} [callback] - Optional callback (err, peers).
   */
  lookup(topic, callback) {
    if (!this.dht || !this.isRunning) {
      const err = new Error('Node must be started before looking up');
      if (callback) callback(err);
      else throw err;
    }
    const topicBuffer = Buffer.isBuffer(topic) ? topic : Buffer.from(topic);
    this.dht.lookup(topicBuffer, callback);
  }

  /**
   * Gets the public IP address and port using STUN servers.
   * This is important for NAT traversal.
   */
  async _getPublicAddress() {
    if (!this.stunServers || this.stunServers.length === 0) {
      console.warn('No STUN servers configured. NAT traversal may be limited.');
      return;
    }

    // Try each STUN server until we get a valid response
    for (const server of this.stunServers) {
      try {
        const stunClient = new StunClient({
          stunHost: server.urls.split(':')[1].replace('//', ''),
          stunPort: parseInt(server.urls.split(':')[2]),
          timeout: 5000
        });
        
        const addr = await stunClient.getMappedAddress();
        this.publicAddress = addr;
        console.log(`Public address detected: ${addr.ip}:${addr.port}`);
        return addr;
      } catch (err) {
        console.warn(`Failed to get public address from ${server.urls}:`, err.message);
      }
    }
    
    console.warn('Could not determine public address via STUN servers.');
    return null;
  }

  /**
   * Connects to the signaling server for connection establishment.
   */
  async _connectToSignalingServer() {
    try {
      // This is a simplified version - in practice, you'd use a more robust
      // WebSocket or Socket.IO client here
      const io = require('socket.io-client');
      this.socket = io(this.signalingServerUrl);
      
      this.socket.on('connect', () => {
        console.log('Connected to signaling server');
        // Register with the signaling server
        this.socket.emit('register', { nodeId: this.nodeId.toString('hex') });
        this.emit('signalingConnected');
      });
      
      this.socket.on('disconnect', () => {
        console.log('Disconnected from signaling server');
        this.emit('signalingDisconnected');
      });
      
      // Handle offers from other nodes
      this.socket.on('offer', async (data) => {
        console.log('Received offer from:', data.from);
        // In a real implementation, you'd pass this to the connection manager
        // For now, we'll handle the connection establishment here
        await this._handleOffer(data);
      });
      
      // Handle answers from other nodes
      this.socket.on('answer', (data) => {
        console.log('Received answer from:', data.nodeId);
        // In a real implementation, this would be processed by the connection manager
        this._handleAnswer(data);
      });
      
      // Handle ICE candidates (if trickle ICE is used in the future)
      this.socket.on('candidate', (data) => {
        console.log('Received ICE candidate from:', data.from);
        // In a real implementation, this would be processed by the connection manager
        this._handleCandidate(data);
      });
      
      // Wait for connection to be established (timeout after 10 seconds)
      await new Promise((resolve, reject) => {
        const timeout = setTimeout(() => {
          reject(new Error('Signaling server connection timeout'));
        }, 10000);
        
        this.once('signalingConnected', () => {
          clearTimeout(timeout);
          resolve();
        });
      });
    } catch (err) {
      console.error('Failed to connect to signaling server:', err);
      this.emit('error', err);
    }
  }

  /**
   * Internal method to handle an incoming offer from another node.
   * @private
   */
  async _handleOffer(data) {
    // In a real implementation, this would delegate to the connection manager
    console.log('Handling offer from:', data.from);
    
    // For demo purposes, we'll simulate sending an answer
    // In real world, this would create a SimplePeer connection
    console.log(`Sending answer to ${data.from}`);
  }

  /**
   * Internal method to handle an incoming answer from another node.
   * @private
   */
  _handleAnswer(data) {
    console.log('Handling answer from:', data.nodeId);
    // In a real implementation, this would complete the connection setup
  }

  /**
   * Internal method to handle an incoming ICE candidate.
   * @private
   */
  _handleCandidate(data) {
    console.log('Handling candidate from:', data.from);
    // In a real implementation, this would be used for NAT traversal
  }

  /**
   * Internal method to load or generate node identity.
   * @private
   */
  async _loadIdentity() {
    if (!this.identityFile) return;

    try {
      const data = await fs.readFile(this.identityFile, 'utf8');
      const identity = JSON.parse(data);
      if (identity.nodeId) {
        this.nodeId = Buffer.from(identity.nodeId, 'hex');
      }
      // Load encryption keys if stored
      console.log('Loaded identity from file');
    } catch (err) {
      if (err.code === 'ENOENT') {
        console.log('Identity file not found, will generate a new one');
      } else {
        console.error('Error loading identity file:', err);
      }
    }
  }

  /**
   * Internal method to save node identity.
   * @private
   */
  async _saveIdentity() {
    if (!this.identityFile) return;

    const identity = {
      nodeId: this.nodeId.toString('hex'),
      // Save encryption keys if needed
    };

    try {
      await fs.writeFile(this.identityFile, JSON.stringify(identity, null, 2));
      console.log('Saved identity to file');
    } catch (err) {
      console.error('Error saving identity file:', err);
    }
  }

  /**
   * Handles incoming UDP messages. Determines if they are for DHT or application.
   * @param {Buffer} msg - The received message buffer.
   * @param {Object} rinfo - Remote address info.
   * @private
   */
  _handleUdpMessage(msg, rinfo) {
    // Pass the message to the connection manager to handle connection-related messages
    this.connectionManager.handleMessage(msg, rinfo);
    
    // Messages not handled by connection manager are DHT messages
    // which are handled automatically by the DHT instance
  }

  /**
   * Handles peer discovery events from the DHT.
   * @param {Buffer} peerInfo - Info about the discovered peer.
   * @param {Buffer} infoHash - The info hash (topic) the peer was found for.
   * @param {string} via - How the peer was found.
   * @private
   */
  _onDhtPeer(peerInfo, infoHash, via) {
    console.log(`DHT Peer found: ${peerInfo.host}:${peerInfo.port} for infoHash ${infoHash.toString('hex')}`);
    // Emit a generic 'peer' event with the topic
    this.emit('peer', peerInfo, infoHash.toString('hex'));
  }

  /**
   * Connects to a discovered peer.
   * @param {Object} peerInfo - Object containing host and port of the peer.
   * @returns {Promise<Object>} A promise that resolves with a connection object.
   */
  async connectToPeer(peerInfo) {
    // Delegate to the connection manager
    return this.connectionManager.connectToPeer(peerInfo);
  }

  /**
   * Sends an encrypted message to a connected peer.
   * @param {Object} connection - The connection object obtained from connectToPeer.
   * @param {string|Buffer} message - The message to send.
   */
  async sendMessage(connection, message) {
    // In a real implementation, this would go through the connection manager
    connection.write(message);
  }
}

module.exports = P2PNode;